/*
 * Tulalip ORM Library, a port of the popular ORMLite library with extensions.
 * Copyright (C) 2012 Richard Schilling
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.j256.ormlite.android.apptools;

import java.lang.reflect.Constructor;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

import android.content.Context;
import android.content.res.Resources;
import android.database.sqlite.SQLiteOpenHelper;

import com.j256.ormlite.dao.BaseDaoImpl;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.logger.Logger;
import com.j256.ormlite.logger.LoggerFactory;

/**
 * This helps organize and access database connections to optimize connection
 * sharing. There are several schemes to manage the database connections in an
 * Android app, but as an app gets more complicated, there are many potential
 * places where database locks can occur. This class allows database connection
 * sharing between multiple threads in a single app. This gets injected or
 * called with the {@link OrmLiteSqliteOpenHelper} class that is used to manage
 * the database connection. The helper instance will be kept in a static field
 * and only released once its internal usage count goes to 0. The
 * {@link SQLiteOpenHelper} and database classes maintain one connection under
 * the hood, and prevent locks in the java code. Creating multiple connections
 * can potentially be a source of trouble. This class shares the same connection
 * instance between multiple clients, which will allow multiple activities and
 * services to run at the same time. Every time you use the helper, you should
 * call {@link #getHelper(Context)} or {@link #getHelper(Context, Class)}. When
 * you are done with the helper you should call {@link #releaseHelper()}.
 * 
 * @author graywatson, kevingalligan
 */
public class OpenHelperManager {

    private static final String HELPER_CLASS_RESOURCE_NAME = "open_helper_classname";
    private static Logger logger = LoggerFactory.getLogger(OpenHelperManager.class);

    private static Class<? extends OrmLiteSqliteOpenHelper> helperClass = null;
    private static volatile OrmLiteSqliteOpenHelper helper = null;
    private static boolean wasClosed = false;
    private static int instanceCount = 0;

    /**
     * If you are _not_ using the {@link OrmLiteBaseActivity} type classes then
     * you will need to call this in a static method in your code.
     */
    public static synchronized void setOpenHelperClass(
            Class<? extends OrmLiteSqliteOpenHelper> openHelperClass) {
        if (openHelperClass == null) {
            helperClass = null;
        } else {
            innerSetHelperClass(openHelperClass);
        }
    }

    /**
     * Set the helper for the manager. This is most likely used for testing
     * purposes and should only be called if you _really_ know what you are
     * doing. If you do use it then it should be in a static {} initializing
     * block to make sure you have one helper instance for your application.
     */
    public static synchronized void setHelper(OrmLiteSqliteOpenHelper helper) {
        OpenHelperManager.helper = helper;
    }

    /**
     * Create a static instance of our open helper from the helper class. This
     * has a usage counter on it so make sure all calls to this method have an
     * associated call to {@link #releaseHelper()}. This should be called during
     * an onCreate() type of method when the application or service is starting.
     * The caller should then keep the helper around until it is shutting down
     * when {@link #releaseHelper()} should be called.
     */
    public static synchronized <T extends OrmLiteSqliteOpenHelper> T getHelper(Context context,
            Class<T> openHelperClass) {
        innerSetHelperClass(openHelperClass);
        return loadHelper(context, openHelperClass);
    }

    /**
     * Similar to {@link #getHelper(Context, Class)} (which is recommended)
     * except we have to find the helper class through other means. This method
     * requires that the Context be a class that extends one of ORMLite's
     * Android base classes such as {@link OrmLiteBaseActivity}. Either that or
     * the helper class needs to be set in the strings.xml.
     * <p>
     * To find the helper class, this does the following: <br />
     * 1) If the class has been set with a call to
     * {@link #setOpenHelperClass(Class)}, it will be used to construct a
     * helper. <br />
     * 2) If the resource class name is configured in the strings.xml file it
     * will be used. <br />
     * 3) The context class hierarchy is walked looking at the generic
     * parameters for a class extending OrmLiteSqliteOpenHelper. This is used by
     * the {@link OrmLiteBaseActivity} and other base classes. <br />
     * 4) An exception is thrown saying that it was not able to set the helper
     * class.
     * </p>
     * 
     * @deprecated Should use {@link #getHelper(Context, Class)}
     */
    @Deprecated
    public static synchronized OrmLiteSqliteOpenHelper getHelper(Context context) {
        if (helperClass == null) {
            if (context == null) {
                throw new IllegalArgumentException("context argument is null");
            }
            Context appContext = context.getApplicationContext();
            innerSetHelperClass(lookupHelperClass(appContext, context.getClass()));
        }
        return loadHelper(context, helperClass);
    }

    /**
     * @deprecated This has been renamed to be {@link #releaseHelper()}.
     */
    @Deprecated
    public static void release() {
        releaseHelper();
    }

    /**
     * Release the helper that was previously returned by a call
     * {@link #getHelper(Context)} or {@link #getHelper(Context, Class)}. This
     * will decrement the usage counter and close the helper if the counter is
     * 0.
     * <p>
     * <b> WARNING: </b> This should be called in an onDestroy() type of method
     * when your application or service is terminating or if your code is no
     * longer going to use the helper or derived DAOs in any way. _Don't_ call
     * this method if you expect to call {@link #getHelper(Context)} again
     * before the application terminates.
     * </p>
     */
    public static synchronized void releaseHelper() {
        instanceCount--;
        logger.trace("releasing helper {}, instance count = {}", helper, instanceCount);
        if (instanceCount <= 0) {
            if (helper != null) {
                logger.trace("zero instances, closing helper {}", helper);
                helper.close();
                helper = null;
                wasClosed = true;
            }
            if (instanceCount < 0) {
                logger.error("too many calls to release helper, instance count = {}", instanceCount);
            }
        }
    }

    /**
     * Set the helper class and make sure we aren't changing it to another
     * class.
     */
    private static void innerSetHelperClass(Class<? extends OrmLiteSqliteOpenHelper> openHelperClass) {
        // make sure if that there are not 2 helper classes in an application
        if (helperClass == null) {
            helperClass = openHelperClass;
        } else if (helperClass != openHelperClass) {
            throw new IllegalStateException("Helper class was " + helperClass
                    + " but is trying to be reset to " + openHelperClass);
        }
    }

    private static <T extends OrmLiteSqliteOpenHelper> T loadHelper(Context context,
            Class<T> openHelperClass) {
        if (helper == null) {
            if (wasClosed) {
                // this can happen if you are calling get/release and then get
                // again
                logger.info("helper was already closed and is being re-opened");
            }
            if (context == null) {
                throw new IllegalArgumentException("context argument is null");
            }
            Context appContext = context.getApplicationContext();
            helper = constructHelper(appContext, helperClass);
            logger.trace("zero instances, created helper {}", helper);
            /*
             * Filipe Leandro and I worked on this bug for like 10 hours
             * straight. It's a doosey. Each ForeignCollection has internal DAO
             * objects that are holding a ConnectionSource. Each Android
             * ConnectionSource is tied to a particular database connection.
             * What Filipe was seeing was that when all of his views we closed
             * (onDestroy), but his application WAS NOT FULLY KILLED, the first
             * View.onCreate() method would open a new connection to the
             * database. Fine. But because he application was still in memory,
             * the static BaseDaoImpl default cache had not been cleared and was
             * containing cached objects with ForeignCollections. The
             * ForeignCollections still had references to the DAOs that had been
             * opened with old ConnectionSource objects and therefore the old
             * database connection. Using those cached collections would cause
             * exceptions saying that you were trying to work with a database
             * that had already been close. Now, whenever we create a new helper
             * object, we must make sure that the internal object caches have
             * been fully cleared. This is a good lesson for anyone that is
             * holding objects around after they have closed connections to the
             * database or re-created the DAOs on a different connection
             * somehow.
             */
            BaseDaoImpl.clearAllInternalObjectCaches();
            /*
             * Might as well do this also since if the helper changes then the
             * ConnectionSource will change so no one is going to have a cache
             * hit on the old DAOs anyway. All they are doing is holding memory.
             * NOTE: we don't want to clear the config map.
             */
            DaoManager.clearDaoCache();
            instanceCount = 0;
        }

        instanceCount++;
        logger.trace("returning helper {}, instance count = {} ", helper, instanceCount);
        @SuppressWarnings("unchecked")
        T castHelper = (T) helper;
        return castHelper;
    }

    /**
     * Call the constructor on our helper class.
     */
    private static OrmLiteSqliteOpenHelper constructHelper(Context context,
            Class<? extends OrmLiteSqliteOpenHelper> openHelperClass) {
        Constructor<?> constructor;
        try {
            constructor = openHelperClass.getConstructor(Context.class);
        } catch (Exception e) {
            throw new IllegalStateException(
                    "Could not find constructor that hast just a (Context) argument for helper class "
                            + openHelperClass, e);
        }
        try {
            return (OrmLiteSqliteOpenHelper) constructor.newInstance(context);
        } catch (Exception e) {
            throw new IllegalStateException("Could not construct instance of helper class "
                    + openHelperClass, e);
        }
    }

    /**
     * Lookup the helper class either from the resource string or by looking for
     * a generic parameter.
     */
    private static Class<? extends OrmLiteSqliteOpenHelper> lookupHelperClass(Context context,
            Class<?> componentClass) {

        // see if we have the magic resource class name set
        Resources resources = context.getResources();
        int resourceId = resources.getIdentifier(HELPER_CLASS_RESOURCE_NAME, "string",
                context.getPackageName());
        if (resourceId != 0) {
            String className = resources.getString(resourceId);
            try {
                @SuppressWarnings("unchecked")
                Class<? extends OrmLiteSqliteOpenHelper> castClass = (Class<? extends OrmLiteSqliteOpenHelper>) Class
                        .forName(className);
                return castClass;
            } catch (Exception e) {
                throw new IllegalStateException("Could not create helper instance for class "
                        + className, e);
            }
        }

        // try walking the context class to see if we can get the
        // OrmLiteSqliteOpenHelper from a generic parameter
        for (Class<?> componentClassWalk = componentClass; componentClassWalk != null; componentClassWalk = componentClassWalk
                .getSuperclass()) {
            Type superType = componentClassWalk.getGenericSuperclass();
            if (superType == null || !(superType instanceof ParameterizedType)) {
                continue;
            }
            // get the generic type arguments
            Type[] types = ((ParameterizedType) superType).getActualTypeArguments();
            // defense
            if (types == null || types.length == 0) {
                continue;
            }
            for (Type type : types) {
                // defense
                if (!(type instanceof Class)) {
                    continue;
                }
                Class<?> clazz = (Class<?>) type;
                if (OrmLiteSqliteOpenHelper.class.isAssignableFrom(clazz)) {
                    @SuppressWarnings("unchecked")
                    Class<? extends OrmLiteSqliteOpenHelper> castOpenHelperClass = (Class<? extends OrmLiteSqliteOpenHelper>) clazz;
                    return castOpenHelperClass;
                }
            }
        }
        throw new IllegalStateException(
                "Could not find OpenHelperClass because none of the generic parameters of class "
                        + componentClass
                        + " extends OrmLiteSqliteOpenHelper.  You should use getHelper(Context, Class) instead.");
    }
}
